package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type LogsInfo struct {
	Ip        string
	Time      int64
	Method    string
	Path      string
	Protocol  string
	Status    int
	Size      int
	Referer   string
	UserAgent string
}

type LogsInfoException struct {
	Ip    string
	Time  int64
	Other string
}

func connMongo() *mongo.Collection {
	// 设置客户端连接配置, mongodb://用户名:密码@host:port/数据库
	clientOptions := options.Client().ApplyURI("mongodb://root:root123@localhost:27017/admin")

	// 连接到MongoDB
	client, err := mongo.Connect(context.TODO(), clientOptions)
	if err != nil {
		log.Fatal(err)
	}

	// 检查连接
	err = client.Ping(context.TODO(), nil)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Connected to MongoDB!")

	// 指定获取要操作的数据集
	return client.Database("test").Collection("nginx_log")
}

func main() {

	start := time.Now()

	// 连接mongodb
	collection := connMongo()

	mq := make(chan string, 50)  // 存放消息，用来给多个go程消费
	signal := make(chan bool, 2) // 读取文件结束标志

	// 注塑主进程，防止主进程exit
	forever := make(chan bool)

	// 起5个go程，解析nginx的mq，并存入到mongodb
	go worker("worker1", mq, collection)
	go worker("worker2", mq, collection)
	go worker("worker3", mq, collection)
	go worker("worker4", mq, collection)
	go worker("worker5", mq, collection)

	// 起一个go程，用来读取nginx
	go task(mq, signal)

	// 主进程退出检测
	go func(signal chan bool, mq chan string, forever chan bool) {
		for {
			if len(signal) == 1 && len(mq) == 0 {
				forever <- true
			}
		}
	}(signal, mq, forever)

	// 优雅退出
	<-forever

	fmt.Println("cost:", time.Since(start).Seconds(), "s")

}

// task
// read log file
// 日志文件一般都比较大，很难一次性读取到内存中
// 这里是一行一行读取，并将每一行string放入mq通道中
func task(mq chan string, signal chan bool) error {

	// read log file
	filePath := "log/access.log"
	f, err := os.Open(filePath)
	defer f.Close()
	if err != nil {
		log.Panic(err)
		return err
	}
	buf := bufio.NewReader(f)

	count := 0
	for {
		if count > 1000 { // 加个条件方便测试数据量
			signal <- true
			return nil
		}
		line, _, err := buf.ReadLine()
		if err != nil {
			if err == io.EOF {
				signal <- true
				return nil
			}
			signal <- true
			return err
		}

		body := strings.TrimSpace(string(line))
		mq <- body
		log.Println("task: ", count)

		count += 1

		// 每次读取后，都检查一下mq通道的容量，如果容量达到一般，则sleep，将时间片交给其他go程
		// 达到容量的阈值，以及sleep的时间，要根据自己的电脑配置计算的出
		// 比如，处理10000个mq，就可以大概计算出处理1个mq所需要的的时间
		if len(mq) > 25 {
			time.Sleep(10 * time.Millisecond)
		}
	}
}

// consumer
// parse and write
func worker(workerName string, mq chan string, collection *mongo.Collection) {
	count := 0
	for d := range mq {
		parse(d, collection)
		count++
		log.Println(workerName, ": ", count)
	}
}

// parse and write
func parse(str string, collection *mongo.Collection) {
	// 每行nginx格式：ip 访问时间 访问方式 访问路径 协议 状态码 数据大小 referer user-agent
	re := `^([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}) - - \[(.*)\] "([^\s]+) ([^\s]+) ([^\s]+?)" ([\d]{3}) ([\d]{1,9}) "([^"]*?)" "([^"]*?)"`
	reg := regexp.MustCompile(re)

	parseInfo := reg.FindStringSubmatch(str)

	// 匹配不到正常的格式，那么这条访问记录很可能有问题
	// 异常nginx处理
	if len(parseInfo) == 0 {
		re1 := `^([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}) - - \[(.*)\] (.*)`
		reg1 := regexp.MustCompile(re1)
		parseInfo1 := reg1.FindStringSubmatch(str)
		if len(parseInfo1) == 0 {
			return
		}

		t1, _ := time.Parse("02/Jan/2006:15:04:05 -0700", parseInfo1[2])
		infoException := LogsInfoException{
			Ip:    parseInfo1[1],
			Time:  t1.Unix(),
			Other: parseInfo1[3],
		}

		// 将异常的nginx日志写入mongodb集合
		collection.InsertOne(context.TODO(), infoException)
		return
	}

	t, _ := time.Parse("02/Jan/2006:15:04:05 -0700", parseInfo[2])
	status, _ := strconv.Atoi(parseInfo[6])
	size, _ := strconv.Atoi(parseInfo[7])

	//
	info := LogsInfo{
		Ip:        parseInfo[1],
		Time:      t.Unix(),
		Method:    parseInfo[3],
		Path:      parseInfo[4],
		Protocol:  parseInfo[5],
		Status:    status,
		Size:      size,
		Referer:   parseInfo[8],
		UserAgent: parseInfo[9],
	}

	// 将正常的nginx日志写入mongodb集合
	collection.InsertOne(context.TODO(), info)
}
